﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Buddy.BehaviorTree;
using Buddy.Common;
using Buddy.Common.Math;
using Buddy.CommonBot;
using Buddy.Navigation;
using Buddy.Swtor;
using Buddy.Swtor.Objects;

using WingIt.Routines;
using WingIt.Routines.Mirrors;

using Action = Buddy.BehaviorTree.Action;


namespace WingIt
{
    [RevisionControlId("$Id: Movement.cs 695 2012-08-13 17:15:25Z Chinajade $")]
    public static partial class CreateBTPS
    {
        /// <summary>
        /// <para>Convenience wrapper around <c>MoveWithinRangeAndLoS(CharacterSelectionDelegate targetDelegate,
        /// RangeProviderDelegate maxRangeDelegate, CanRunDecoratorDelegate doWhen)</c></para>
        /// </summary>
        /// <param name="targetDelegate"></param>
        /// <param name="maxRange"></param>
        /// <param name="doWhen"></param>
        /// <returns></returns>
        ///  8Jul2012-17:13UTC chinajade
        public static Composite MoveWithinRangeAndLoS(CharacterSelectionDelegate targetDelegate, float maxRange, CanRunDecoratorDelegate doWhen = null)
        {
            return MoveWithinRangeAndLoS(targetDelegate, rangeContext => maxRange, doWhen);
        }

        /// <summary>
        /// <para>If DOWHEN evaluates to true, move within MAXRANGE and within line-of-sight of the target identified by TARGETDELEGATE.</para>
        /// <para>Notes:<list type="bullet">
        /// <item><description><para> * If DOWHEN is omitted, the behavior will always be attempted.</para></description></item>
        /// <item><description><para> * MAXRANGE must be greater than or equal to 0.0f.</para></description></item>
        /// <item><description><para> * When this behavior completes, the character may occasionally be in motion for a short time after.
        /// If you want to completely stop, follow this behavior with a CreateBTPS.StopMoving() behavior.</para></description></item>
        /// </list></para>
        /// </summary>
        /// <param name="targetDelegate"></param>
        /// <param name="maxRange"></param>
        /// <param name="doWhen"></param>
        /// <returns></returns>
        ///  8Jul2012-17:13UTC chinajade
        // NB: It is tempting to include a StopMoving() as part of ths behavior; however, please refrain from doing so.
        // Not all classes require a cessation of movement before they can start their attacks (e.g., insta-casts).
        // Instead, the caller should explicitly follow calls of this behavior with a CreateBTPS.StopMoving() behavior,
        // if that is what the caller requires.
        public static Composite MoveWithinRangeAndLoS(CharacterSelectionDelegate targetDelegate, RangeProviderDelegate maxRangeDelegate, CanRunDecoratorDelegate doWhen = null)
        {
            const int MaxTryCount_LineOfSightBeforeChokeUp = 2;

            Logger.ContractRequires(() => targetDelegate != null, () => "targetDelegate != null");
            Logger.ContractRequires(() => maxRangeDelegate != null, () => "maxRangeDelegate != null");

            doWhen = doWhen ?? (ctx => true);       // Resolve optional argument to something sane

            return new Decorator(whenMovementAllowed => doWhen(whenMovementAllowed) && LazyRaider.IsMovementEnabled
                                                        && (!targetDelegate(whenMovementAllowed).IsInRange(maxRangeDelegate(whenMovementAllowed))
                                                            || !targetDelegate(whenMovementAllowed).InLineOfSight),
                new PrioritySelector(
                    new Action(moveWhen =>
                    {
                        TorCharacter currentTarget = targetDelegate(moveWhen);
                        if ((moveWithinRangeAndLoS_ChokeUpDistance == null) || (currentTarget != moveWithinRangeAndLoS_LastChokeUpTarget))
                        {
                            moveWithinRangeAndLoS_TryCountLos = 0;
                            moveWithinRangeAndLoS_LastChokeUpTarget = currentTarget;
                            moveWithinRangeAndLoS_ChokeUpDistance = maxRangeDelegate(moveWhen);
                        }

                        if (!currentTarget.IsInRange(moveWithinRangeAndLoS_ChokeUpDistance.Value))
                        {
                            Navigator.MoveTo(currentTarget.Position);
                            return RunStatus.Success;
                        }

                        // If we're having trouble acquiring LoS, move closer...
                        if (++moveWithinRangeAndLoS_TryCountLos > MaxTryCount_LineOfSightBeforeChokeUp)
                        {
                            moveWithinRangeAndLoS_TryCountLos = 0;
                            moveWithinRangeAndLoS_ChokeUpDistance = CalculateChokeDistance(moveWithinRangeAndLoS_ChokeUpDistance.Value);
                            return RunStatus.Success;
                        }

                        return RunStatus.Failure; // fall through
                    }),
                    CommonBehaviors.MoveToLos(myTarget => targetDelegate(myTarget), true)
                ));
        }
        private static float? moveWithinRangeAndLoS_ChokeUpDistance = null;
        private static TorCharacter moveWithinRangeAndLoS_LastChokeUpTarget = null;
        private static int moveWithinRangeAndLoS_TryCountLos = 0;

        /// <summary>
        /// <para>Convenience wrapper around <c>StopWithinRangeAndLoS(CharacterSelectionDelegate targetDelegate,
        /// RangeProviderDelegate maxRangeDelegate, CanRunDecoratorDelegate doWhen)</c></para>
        /// </summary>
        /// <param name="targetDelegate"></param>
        /// <param name="maxRange"></param>
        /// <param name="doWhen"></param>
        /// <returns></returns>
        ///  8Jul2012-17:13UTC chinajade
        public static Composite StopWithinRangeAndLoS(CharacterSelectionDelegate targetDelegate, float maxRange, CanRunDecoratorDelegate doWhen = null)
        {
            return StopWithinRangeAndLoS(targetDelegate, maxRangeContext => maxRange, doWhen);
        }

        /// <summary>
        /// <para>If DOWHEN evaluates to true, stops toon movement.</para>
        /// <para>Notes:<list type="bullet">
        /// <item><description><para> * If DOWHEN is omitted, the behavior will always be attempted.</para></description></item>
        /// <item><description><para> * Attempts to stop toon movement may fail in some situations.
        /// For instance, the toon may be sliding down a hill.
        /// When throttling happens, a message is emitted to the log.</para></description></item>
        /// <item><description><para> * Failed attempts to stop movement are throttled so other behavior tree actions can take place.
        /// When throttling happens, a message is emitted to the log.</para></description></item>
        /// <item><description><para> * Failure to stop moving can happen due to lag, client stuttering, low frame rates, game terrain, and other things.</para></description></item>
        /// </list></para>
        /// </summary>
        /// <returns><para>a behavior tree Composite for use in a PrioritySelector.</para>
        /// <para>The Composite returns RunStatus.Success to the BT parent if the behavior is in progress.
        /// Otherwise, returns RunStatus.Failure indicating no action was necessary.</para>
        /// </returns>
        ///  7Jul2012-20:27UTC chinajade
        public static Composite StopWithinRangeAndLoS(CharacterSelectionDelegate targetDelegate, RangeProviderDelegate maxRangeDelegate, CanRunDecoratorDelegate doWhen = null)
        {
            doWhen = doWhen ?? (ctx => true);       // Resolve optional argument to something sane

            return new Decorator(whenMovementAllowed => doWhen(whenMovementAllowed) && LazyRaider.IsMovementEnabled && Me.IsMoving
                                                        && targetDelegate(whenMovementAllowed).IsInRange(maxRangeDelegate(whenMovementAllowed))
                                                        && targetDelegate(whenMovementAllowed).InLineOfSight,
                new Action(stopMoving => Navigator.MovementProvider.StopMovement()));
            // NB: Don't be tempted to return RunStatus.Failure to better performance...
            // Many behaviors depend on the toon _actually_ being stopped in the correct place.  The implicit RunStatus.Success
            // return means this behavior will be retried if stopping fails, such as we're sliding down a hill.
        }

        /// <summary>
        /// Moves behind the target(Still BETA!)
        /// </summary>
        /// Neo93
        public static Composite MoveBehind(TorCharacter Target, float range)
        {
            /*return !BuddyTor.Me.IsBehind(Target) ? 
                new Sequence(
                    new Action(moveTo => MoveTo(location => Target.Position, 0)),
                    new Action(moveBehind => Buddy.Swtor.Movement.Move(MovementDirection.Forward, TimeSpan.FromMilliseconds(50)))*/
            return new Decorator(whenMovementEnabled => LazyRaider.IsMovementEnabled && !Me.IsCasting,
                new PrioritySelector(
                    new Decorator(whenNotBehind => !Me.IsBehind(Target),
                        new Sequence(
                            CommonBehaviors.MoveAndStop(location => Target.Position, 0.0f, true, "Target Position"),
                            new Action(doMoveBehind => Buddy.Swtor.Movement.Move(MovementDirection.Forward, TimeSpan.FromMilliseconds(50)))
                            )),
                    CommonBehaviors.MoveAndStop(location => Target.Position, range, true, "Target Position")
                    ));
        }

        /// <summary>
        /// <para>If DOWHEN evaluates to true, crouches in a location appropriate to attack the target identified by TARGETDELEGATE.</para>
        /// <para>The behavior takes whatever actions necessary to move within range and line-of-sight of the target, before crouching.</para>
        /// <para>Notes:<list type="bullet">
        /// <item><description><para> * If DOWHEN is omitted, the behavior will always be attempted.</para></description></item>
        /// <item><description><para> * This behavior is only for Agents and Smuggler use.
        /// Use by any other class will result in a MaintenanceError.</para></description></item>
        /// <item><description><para> * Crouching will sometimes fail.
        /// This behavior throttles failed attempts to crouch so other activities can take place.
        /// When throttling happens, a message is emitted to the log.</para></description></item>
        /// <item><description><para> * Failure to crouch can happen due to lag, client stuttering, low frame rates, game terrain, and other things.</para></description></item>
        /// </list></para>
        /// </summary>
        /// <param name="targetDelegate"></param>
        /// <param name="doWhen"></param>
        /// <returns><para>a behavior tree Composite for use in a PrioritySelector.</para>
        /// <para>The Composite returns RunStatus.Success to the BT parent if the behavior is in progress.
        /// Otherwise, returns RunStatus.Failure indicating no action was necessary.</para>
        /// </returns>
        ///  8Aug2012-06:25UTC chinajade
        public static Composite MoveAndCoverForTarget(CharacterSelectionDelegate targetDelegate,
                                                      RangeProviderDelegate maxRangeDelegate = null,
                                                      CanRunDecoratorDelegate doWhen = null)
        {
            TorAbility crouchAbility = BuddyTor.Me.GetMirrorAbility("Crouch");
            TorAbility takeCoverAbility = BuddyTor.Me.GetMirrorAbility("Take Cover");
            const int MaxCover_TryCount = 3;
            const int MaxCover_VisitsCountAtPull = 2;
            string MyMethodName = Logger.GetMyMethodName();

            Logger.ContractRequires(() => targetDelegate != null, () => "targetDelegate != null");

            if (!((Me.Class == CharacterClass.Agent) || (Me.Class == CharacterClass.Smuggler)))
            {
                Logger.MaintenanceError("The {0} behavior is only useful for Agent- or Smuggler-based classes."
                                        + "\n    Your class is {1}.", MyMethodName, Me.Class);
            }

            // Resolve optional arguments to something sane
            doWhen = doWhen ?? (ctx => true);
            maxRangeDelegate = maxRangeDelegate ?? (r => 2.95f);

            // This is built as a rather complex state-machine, because all the bugs in SWtOR made us do it this way...
            // As a general rule, the state machine should report RunStatus.Success to the BT parent whenever it changes state; otherwise,
            // it reports RunStatus.Failure.
            // NB: When altering the state machine, please recall that our state may go 'stale' due to external influences--
            // such as being knocked out of cover, or one of our explosive attacks knocking our target out of range.
            // The state machine must detect these situations and recover gracefully.f
            return new Decorator(whenMovementEnabled => doWhen(whenMovementEnabled) && LazyRaider.IsMovementEnabled,
                new PrioritySelector(
                    //Info we need when people report problems...
                    //DebugTools.CreateBTPS_InfoDump(infoDump => {
                    //    TorCharacter myTarget = targetDelegate(infoDump);
                    //    return string.Format("Result:{5} CoverState:{0}({1}), '{2}' at {3:f1}{4} meters",
                    //                    coverState.ToString(),
                    //                    (Me.HasMirrorBuff("Cover") ? "TakeCover" : Me.HasMirrorBuff("Crouch") ? "Crouch" : "None"),
                    //                    myTarget.LoggableIdentifier(), (myTarget.Distance * 10),
                    //                    ((coverState == CoverState.ClosingDistance) ? string.Format("/{0:f1}({1})", coverDistanceNeeded * 10, (myTarget.InLineOfSight ? "LoS" : "No-LoS")) : ""),
                    //                    Me.IsAbilityReady(BuddyTor.Me.GetMirrorAbility("Rifle Shot"), myTarget).ToString());
                    //}),

                    new Decorator(coveredWhen => coverState == CoverState.Covered,
                        new Action(coverNoLongerValidWhen =>
                        {
                            TorCharacter currentTarget = targetDelegate(coverNoLongerValidWhen);
                            tryCount_CoverAttempts = 0;

                            // If cover not sufficient to address current target...
                            // NB: simply changing targets is insufficient to declare cover as no longer valid.
                            if (!Me.IsInCover() || !currentTarget.IsInRange(maxRangeDelegate(coverNoLongerValidWhen)) || !currentTarget.InLineOfSight)
                                { coverState = CoverState.UnCovered; return RunStatus.Failure; }

                            // This detects terrain glitches, and moves closer to mob if needed...
                            if (IsBugged_TargetViewBlocked(currentTarget))
                            {
                                Logger.Write("Target view blocked for '{0}'--recovering by closing distance", currentTarget.LoggableIdentifier());
                                coverState = CoverState.ChokeUpNeeded; 
                                return RunStatus.Failure;
                            }

                            // This detects when we're unable to acquire the target while covered at the pull...
                            // This method detects a particular SWtOR glitch that looks like this: the toon will TakeCover/Crouch,
                            // but every time it tries to use an ability, a "Cannot See Target" message will show up on the screen.
                            // From a programming viewpoint, this situation is not directly detectable (the EffectResult return code
                            // from the ability use is 'Unknown').  This situation only happens 'at the pull'.
                            // The technique we use here is a brute-force one, that only looks when we're at the pull.  If we visit a few times
                            // without getting into combat, this method decides the bug is active, and notifies the caller for corrective action.
                            if (TargetSelect.IsAtThePull())
                            {
                                if (++coverStateVisitsAtThePull > MaxCover_VisitsCountAtPull)
                                {
                                    coverStateVisitsAtThePull = 0;
                                    coverState = CoverState.ChokeUpNeeded;
                                    return RunStatus.Failure;
                                }
                            }
                            else
                                { coverStateVisitsAtThePull = 0; }

                            return RunStatus.Failure; // fall through
                        })),

                    new Decorator(uncoveredWhen => coverState == CoverState.UnCovered,
                        new PrioritySelector(
                            CreateBTPS.StopWithinRangeAndLoS(myTarget => targetDelegate(myTarget), float.MaxValue), // just stop moving
                            Mirror.Cast(takeCoverAbility.Name), // forces a dismount, if needed

                            new Action(coverStillViable =>
                            {
                                lastCoverTarget = targetDelegate(coverStillViable);

                                // If we've exceeded our tries to take cover, close some distance before trying again...
                                // There are certain places on the terrain where TakeCover/Crouch appears to succeed, but it indeed fails (we
                                // never acquire the buff).  When this happens, we need to move.
                                if (tryCount_CoverAttempts > MaxCover_TryCount)
                                {
                                    tryCount_CoverAttempts = 0;
                                    coverState = CoverState.ChokeUpNeeded;
                                    return RunStatus.Failure; // fall through
                                }

                                // If not in range or LoS, close the distance...
                                if (!lastCoverTarget.IsInRange(maxRangeDelegate(coverStillViable)) || !lastCoverTarget.InLineOfSight)
                                {
                                    coverDistanceNeeded = maxRangeDelegate(coverStillViable);
                                    coverState = CoverState.ClosingDistance;
                                    return RunStatus.Failure; // fall through
                                }

                                // The IsAbilityReady() is used to detect and work around LoS problems caused by terrain glitches...
                                if (IsBugged_TargetViewBlocked(lastCoverTarget))
                                    { coverState = CoverState.ChokeUpNeeded; return RunStatus.Failure; }

                                // If existing cover handles new target, we're good...
                                if (Me.IsInCover())
                                    { coverState = CoverState.Covered; return RunStatus.Success; }

                                coverState = CoverState.TryTakeCoverAndCrouch;
                                return RunStatus.Failure; // fall through
                            })
                            )),

                    new Decorator(tryTakeCoverWhen => coverState == CoverState.TryTakeCoverAndCrouch,
                        new Action(tryTakingCover =>
                        {
                            ++tryCount_CoverAttempts;
                            // Make certain we're targeting the target such that Take Cover position will be calculated correctly...
                            // The TargetSelect.ForAttack() target may not be the same as the CurrentTarget is the reason we must do this.
                            lastCoverTarget.Target();

                            // Prefer "Take Cover"...
                            EffectResult coverResult = EffectResult.Unknown;
                            Logging.Write("Casting " + takeCoverAbility.Name);
                            BuddyTor.Me.AbilityActivate(takeCoverAbility, Me, out coverResult);
                            if (coverResult != EffectResult.Ok)
                            {
                                // "Take Cover" failed, try "Crouch"...
                                coverResult = EffectResult.Unknown;
                                Logging.Write("Casting " + crouchAbility.Name);
                                BuddyTor.Me.AbilityActivate(crouchAbility, Me, out coverResult);
                            }

                            if ((coverResult == EffectResult.Ok) && lastCoverTarget.InLineOfSight)
                            {
                                coverState = CoverState.Covered;
                                return RunStatus.Success;
                            }

                            coverState = CoverState.ChokeUpNeeded;
                            return RunStatus.Failure; // fall through
                        })),

                    new Decorator(chokeUpNeededWhen => coverState == CoverState.ChokeUpNeeded,
                        new Action(chokeUpNeeded =>
                        {
                            TorCharacter currentTarget = targetDelegate(chokeUpNeeded);

                            // TakeCover/Crouch failed or target not in LoS; Close the distance to the target...
                            // TakeCover/Crouch occasionally fails if we're standing on steps or right up against a wall.
                            // The Target LoS can be broken on TakeCover/Crouch for several reasons.  For example,
                            // 1) The target is slightly elevated, and we're too close to the dias base on which the target is standing, or
                            // 2) There is a barrel/box between us and target that is LoS-okay while we're standing, but LoS-impaired
                            //    once we TakeCover/Crouch.
                            // 3) Sometimes there are simply 'landscape glitches' that cause the attack to produce EffectResult.TargetViewBlocked.
                            coverDistanceNeeded = CalculateChokeDistance(currentTarget.Distance);
                            coverState = CoverState.ClosingDistance;
                            return RunStatus.Failure; // fall through
                        })),

                    new Decorator(closingDistanceWhen => coverState == CoverState.ClosingDistance,
                        new PrioritySelector(
                            new Action(closingDistance =>
                            {
                                TorCharacter currentTarget = targetDelegate(closingDistance);

                                // If target changed, we may not need to close distance, or may need a new choke distance...
                                if (currentTarget != lastCoverTarget)
                                    { coverState = CoverState.UnCovered; return RunStatus.Success; }

                                if (currentTarget.IsInRange(coverDistanceNeeded) && currentTarget.InLineOfSight)
                                    { coverState = CoverState.UnCovered; return RunStatus.Success; }

                                if (currentTarget.IsInRange(Global.Distance.Melee * 0.75f))
                                    { coverState = CoverState.CoverFailed; return RunStatus.Success; }

                                return RunStatus.Failure; // fall through
                            }),
                            CreateBTPS.BreakCrouchOrCover(),
                            CreateBTPS.MoveWithinRangeAndLoS(myTarget => targetDelegate(myTarget), chokeDistance => coverDistanceNeeded)
                        )),

                    new Decorator(coverFailedWhen => coverState == CoverState.CoverFailed,
                        new PrioritySelector(
                            new Action(coverFailedResetWhen =>
                            {
                                if (!Me.InCombat || (targetDelegate(coverFailedResetWhen) != lastCoverTarget))
                                    { coverState = CoverState.UnCovered; return RunStatus.Success; }

                                if (Me.IsInCover())
                                    { coverState = CoverState.Covered; return RunStatus.Success; }

                                return RunStatus.Failure;
                            }),
                            Mirror.Cast(crouchAbility.Name, crouchOn => Me)
                        ))
                    ));
        }
        private static float coverDistanceNeeded = Global.Distance.Ranged;
        private static CoverState coverState = CoverState.UnCovered;
        private static int tryCount_CoverAttempts = 0;
        private static int coverStateVisitsAtThePull = 0;
        private static TorCharacter lastCoverTarget = null;
        private enum CoverState
        {
            ChokeUpNeeded,
            ClosingDistance,
            Covered,
            CoverFailed,
            TryTakeCoverAndCrouch,
            UnCovered,
        };

        private static float CalculateChokeDistance(float currentDistance)
        { // 8Aug2012-11:51UTC chinajade
            const float DistanceChokeupMin = 0.3f; // minimum absolute move distance to get closer to target (in decameters)
            const float DistanceChokeupPercent = 0.20f; // percentage move distance to get closer to target
            const float MinDistanceAllowedToTarget = Global.Distance.Melee * 0.75f;

            float distanceByAbsoluteCalculation = currentDistance - DistanceChokeupMin;
            float distanceByPercentageCalculation = currentDistance * (1.0f - DistanceChokeupPercent);

            // NB: We should never return a very tiny or negative distance, thus the MinDistanceAllowedToTarget hard stop...
            return Math.Max(MinDistanceAllowedToTarget, Math.Min(distanceByAbsoluteCalculation, distanceByPercentageCalculation));
        }

        // This method detects a particular SWtOR glitch that looks like this: the SWtOR client reports us
        // as having line-of-sight on the target; however, trying to use an ability returns a TargetViewBlocked
        // result.
        // A target can be InLineOfSight, yet an attack can return a TargetViewBlocked error--go figure.
        // This detects the presence of the dichotomy.
        // 13Aug2012-06:50UTC chinajade
        private static bool IsBugged_TargetViewBlocked(TorCharacter target)
        {
            TorAbility testLosAbility = BuddyTor.Me.GetMirrorAbility("Rifle Shot");

            return Me.IsAbilityReady(testLosAbility, target) == EffectResult.TargetViewBlocked;
        }
    }
}
